package com.cq.cqmall.product.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cq.common.constants.ProductConstant;
import com.cq.common.utils.PageUtils;
import com.cq.common.utils.Query;
import com.cq.cqmall.product.dao.CategoryDao;
import com.cq.cqmall.product.entity.CategoryEntity;
import com.cq.cqmall.product.service.CategoryBrandRelationService;
import com.cq.cqmall.product.service.CategoryService;
import com.cq.cqmall.product.vo.Catalog2VO;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


/**
 * 类别服务impl
 *
 * @author 程崎
 * @since 2022/09/06
 */
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Resource
    private CategoryBrandRelationService categoryBrandRelationService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
        return categoryEntityList
                .stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .peek(menu -> menu.setChildren(getChildren(menu, categoryEntityList)))
                .sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
                .collect(Collectors.toList());
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {

        baseMapper.deleteBatchIds(asList);
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> parentPath = findParentPath(catelogId, new ArrayList<>());
        return parentPath.toArray(new Long[0]);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
/*    @Caching(evict = {
            @CacheEvict(value = "category",key = "'getLevel1Categories'"),
            @CacheEvict(value = "category",key = "'getCatalogJson'")
    })*/
    @CacheEvict(value = "category", allEntries = true)
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

    @Cacheable(value = {"category"}, key = "#root.method.name", sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categories() {
        return this.baseMapper.selectList(
                Wrappers.lambdaQuery(CategoryEntity.class)
                        .eq(CategoryEntity::getParentCid, 0)
        );
    }

    private Map<String, List<Catalog2VO>> getCatalogJsonFromRedis() {
        String newCatalogJson = stringRedisTemplate.opsForValue().get(ProductConstant.CacheEnum.CACHE_CATALOG_JSON.getKey());
        if (StringUtils.isNotEmpty(newCatalogJson)) {
            return JSON.parseObject(newCatalogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
            });
        }
        Map<String, List<Catalog2VO>> dataFromDb = getCatalogJsonFromDb();
        stringRedisTemplate.opsForValue().set(
                ProductConstant.CacheEnum.CACHE_CATALOG_JSON.getKey(),
                JSON.toJSONString(dataFromDb),
                1,
                TimeUnit.DAYS
        );
        return dataFromDb;
    }

    @Override
    @Cacheable(value = "category", key = "#root.methodName", sync = true)
    public Map<String, List<Catalog2VO>> getCatalogJson() {
        return getCatalogJsonFromDb();
    }

    @Deprecated
    public Map<String, List<Catalog2VO>> getCatalogJsonOld() {
        String catalogJson = stringRedisTemplate.opsForValue().get(ProductConstant.CacheEnum.CACHE_CATALOG_JSON.getKey());
        if (StringUtils.isEmpty(catalogJson)) {
            RLock lock = redisson.getLock(ProductConstant.CacheEnum.CACHE_CATALOG_JSON_LOCK.getKey());
            lock.lock();
            Map<String, List<Catalog2VO>> dataFromRedis;
            try {
                dataFromRedis = getCatalogJsonFromRedis();
            } finally {
                lock.unlock();
            }
            return dataFromRedis;
        }
        return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
        });
    }

    @Deprecated
    public Map<String, List<Catalog2VO>> getCatalogJsonWithRedisLock() {
        String catalogJson = stringRedisTemplate.opsForValue().get(ProductConstant.CacheEnum.CACHE_CATALOG_JSON.getKey());
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存未命中");
            String uuid = UUID.randomUUID().toString();
            Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(ProductConstant.CacheEnum.LOCK.getKey(), uuid, 300, TimeUnit.SECONDS);
            if (Boolean.TRUE.equals(lock)) {
                System.out.println("获取分布式锁成功...");
                Map<String, List<Catalog2VO>> catalogJsonFromDb;
                try {
                    catalogJsonFromDb = getCatalogJsonFromRedis();
                } finally {
                    //region 获取值对比+对比成功删除=原子操作 ==> lua脚本。原因：获取到锁后，若在传输过程中，已经有别的线程抢占了锁，那么当前的锁和传输过去的锁其实是不一致的，那么删除还是会删除别人的锁
                    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return  redis.call('del', KEYS[1]) else return 0 end";
                    stringRedisTemplate.execute(new DefaultRedisScript<>(
                                    script, Long.class),
                            Collections.singletonList(ProductConstant.CacheEnum.LOCK.getKey()),
                            uuid
                    );
                    //endregion
                }
                return catalogJsonFromDb;
            } else {
                // 自旋
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("获取分布式锁失败...");
                return getCatalogJsonWithRedisLock();
            }
        }
        System.out.println("缓存命中");
        return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
        });
    }

    @Deprecated
    public Map<String, List<Catalog2VO>> getCatalogJsonWithLocalLock() {
        String catalogJson = stringRedisTemplate.opsForValue().get(ProductConstant.CacheEnum.CACHE_CATALOG_JSON.getKey());
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存未命中");
            synchronized (this) {
                return getCatalogJsonFromRedis();
            }
        }
        System.out.println("缓存命中");
        return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
        });
    }

    public Map<String, List<Catalog2VO>> getCatalogJsonFromDb() {
        // 将多次的数据库查询变为一次
        List<CategoryEntity> selectList = this.baseMapper.selectList(null);
        List<CategoryEntity> level1Categories = getParentCid(selectList, 0L);
        return level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            List<CategoryEntity> categoryEntityList = getParentCid(selectList, v.getCatId());
            List<Catalog2VO> catalog2Vos = new ArrayList<>();
            if (!ObjectUtils.isEmpty(categoryEntityList)) {
                catalog2Vos = categoryEntityList.stream().map(l2 -> {
                    Catalog2VO catalog2Vo = new Catalog2VO(
                            v.getCatId().toString(),
                            null,
                            l2.getCatId().toString(),
                            l2.getName()
                    );
                    List<CategoryEntity> level3Catalog = getParentCid(selectList, l2.getCatId());

                    if (!ObjectUtils.isEmpty(level3Catalog)) {
                        catalog2Vo.setCatalog3List(
                                level3Catalog.stream().map(l3 -> new Catalog2VO.Catalog3VO(
                                        l2.getCatId().toString(),
                                        l3.getCatId().toString(),
                                        l3.getName()
                                )).collect(Collectors.toList())
                        );
                    }

                    return catalog2Vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
    }

    private List<CategoryEntity> getParentCid(List<CategoryEntity> selectList, Long parentCid) {
        return selectList.stream().filter(item -> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
    }

    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        CategoryEntity byId = this.getById(catelogId);
        if (byId.getParentCid() != 0) {
            findParentPath(byId.getParentCid(), paths);
        }
        paths.add(catelogId);
        return paths;
    }

    /**
     * 递归查找所有菜单的子菜单
     *
     * @param root 根
     * @param all  所有
     * @return {@link List}<{@link CategoryEntity}>
     */
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
        return all.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid().equals(root.getCatId()))
                // 找到子菜单
                .peek(categoryEntity -> categoryEntity.setChildren(getChildren(categoryEntity, all)))
                // 菜单排序
                .sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
                .collect(Collectors.toList());
    }

}